/*	--- TCP编程 --- 

socket 
n.	(电源)插座;(电器上的)插口，插孔，管座;托座;孔穴;窝;槽;臼
vt.	把…装入插座;给…配插座
========================

在开发网络应用程序的时候，我们又会遇到Socket这个概念。

Socket是一个抽象概念，一个应用程序通过一个Socket来建立一个远程连接，而Socket内部通过TCP/IP协议把数据传输到网络：

┌───────────┐                                   ┌───────────┐
│Application│                                   │Application│
├───────────┤                                   ├───────────┤
│  Socket   │                                   │  Socket   │
├───────────┤                                   ├───────────┤
│    TCP    │                                   │    TCP    │
├───────────┤      ┌──────┐       ┌──────┐      ├───────────┤
│    IP     │<────>│Router│<─────>│Router│<────>│    IP     │
└───────────┘      └──────┘       └──────┘      └───────────┘
	


Socket、TCP和部分IP的功能都是由【操作系统OS】提供的.

不同的编程语言只是提供了对操作系统调用的简单的封装。例如，Java提供的【几个Socket相关的类】就封装了【操作系统提供的接口】。

Q: 为什么需要Socket进行网络通信？
A: 因为仅仅通过IP地址进行通信是不够的，同一台计算机同一时间会运行【多个网络应用程序】，例如：浏览器、QQ、邮件客户端等。 当操作系统接收到一个数据包的时候，如果只有IP地址，它没法判断应该发给哪个应用程序，所以，操作系统抽象出Socket接口，每个应用程序需要各自对应到不同的Socket，数据包才能根据Socket正确地发到对应的应用程序。

一个Socket就是由IP地址和端口号（范围是0～65535）组成，可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配，它是一个0～65535之间的数字，其中，小于1024的端口属于特权端口，需要管理员权限，大于1024的端口可以由任意用户的应用程序打开。

	· Explorer: 101.202.99.2:1201
	· QQ: 101.202.99.2:1304
	· Mail: 101.202.99.2:15000


使用Socket进行网络编程时，本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端，它会主动监听某个指定的端口，另一个进程必须充当客户端，它必须主动连接服务器的IP地址和指定端口，如果连接成功，服务器端和客户端就成功地建立了一个TCP连接，双方后续就可以随时发送和接收数据。

因此，当Socket连接成功地在服务器端和客户端之间建立后：

	· 对Server端来说，它的Socket是指定的IP地址和指定的端口号,例如: ;
	· 对Client端来说，它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。


--------------------------------------


#	服务器端

要使用Socket编程，我们首先要编写服务器端程序。Java标准库提供了ServerSocket来实现对指定IP和指定端口的监听。ServerSocket的典型实现代码如下：*/
public class Server {
	public static void main(String[] args) throws IOException {
		ServerSocket ss = new ServerSocket(6666); 
		//Server端, 指定监听port=6666(自己的端口,TCP中别人的输入)

		System.out.println("Server is running...");

		for (;;) {
			Socket sock = ss.accept();
			System.out.println("connected from " + sock.getRemoteSocketAddress());

			Thread t = new Handler(sock);
			t.start();
		}
	}
}

// Handler  处理器;句柄;处理程序;处理者;处理
class Handler extends Thread {
	Socket sock;

	public Handler(Socket sock) {
		this.sock = sock;
	}

	@Override
	public void run() {
		try (InputStream input = this.sock.getInputStream()) {
			try (OutputStream output = this.sock.getOutputStream()) {
				handle(input, output);
			}
		} catch (Exception e) {
			try {
				this.sock.close();
			} catch (IOException io_e) {
				//...
			} 

			System.out.println("client disconnected.");
		}
	}

	private void handle(InputStream input, OutputStream output)	throws IOException {
		var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
		var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));

		writer.write("hello\n");
		writer.flush(); // 清空writer缓存并发送到目标socket

		for (;;) {
			String s = reader.readLine();

			if (s.equals("bye")) { // 若socket的reader接收到bye
				writer.write("bye\n"); // 则输出bye给目标socket
				writer.flush(); // 发送
				break;
			}

			writer.write("ok: " + s + "\n"); // ok: reader.readLine()
			writer.flush(); // 发送到目标socket
		}
	}
}


// 服务器端通过代码：
ServerSocket ss = new ServerSocket(6666);

/*
在本机的指定端口port=6666监听。

这里我们没有指定IP地址，表示在计算机的所有网络接口上进行监听。

如果ServerSocket监听成功，我们就使用一个无限循环来处理客户端的连接：*/
for (;;) {
	Socket sock = ss.accept(); // 若有socket接通,则建立一个新的Socket
	Thread t = new Handler(sock); //并用一个线程指向该socket的任务
	t.start();
}


/*
注意到代码ss.accept()表示每当有新的客户端连接进来后，就返回一个Socket实例，这个Socket实例就是用来和刚连接的客户端进行通信的。由于客户端很多，要实现并发处理，我们就必须为每个新的Socket创建一个新线程来处理，这样，主线程的作用就是接收新的连接，每当收到新连接后，就创建一个新线程进行处理。

我们在多线程编程的章节中介绍过线程池，这里也完全可以利用线程池来处理客户端连接，能大大提高运行效率。

如果没有客户端连接进来，accept()方法会阻塞并一直等待。如果有多个客户端同时连接进来，ServerSocket会把连接扔到队列里，然后一个一个处理。对于Java程序而言，只需要通过循环不断调用accept()就可以获取新的连接。


-----------------------------------------------------------


#	客户端

相比服务器端，客户端程序就要简单很多。一个典型的客户端程序如下： */
public class Client {
	public static void main(String[] args) throws IOException {
		Socket sock = new Socket("localhost", 6666);
			// 连接指定的Server的IP地址, 连接Server的哪个端口

		// 输入流/输出流 对接 Socket的输入流/输出流
		try (InputStream input = sock.getInputStream()) {
			try (OutputStream output = sock.getOutputStream()) {
				handle(input, output);
			} 
		}

		sock.close();
		System.out.println("Client disconnected.");
	}

	private static void handle(InputStream input, OutputStream output) throws IOException {
		// 建立 输出流/输入流 的 转型Writer/Reader 的 缓存
		var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
		var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));

		Scanner scanner = new Scanner(System.in); // 读取输入
		System.out.println("[Server] " + reader.readLine());

		for (;;) {
			System.out.print(">>>"); // 打印提示

			String s = scanner.nextLine(); // 读取一行输入
			writer.write(s);
			writer.newLine();
			writer.flush();

			String resp = reader.readLine();
			System.out.println("<<< " + resp);

			if (resp.equals("bye")) {
				break;
			}
		}
	}
}


//客户端程序通过：
Socket sock = new Socket("localhost", 6666);

//连接到服务器端，注意上述代码的服务器地址是"localhost"，表示本机地址，端口号是6666。如果连接成功，将返回一个Socket实例，用于后续通信。



/*--------------------------------------------------------


#	Socket流

当Socket连接创建成功后，无论是服务器端，还是客户端，我们都使用Socket实例进行网络通信。因为TCP是一种基于流的协议，因此，Java标准库使用InputStream和OutputStream来封装Socket的数据流，这样我们使用Socket的流，和普通IO流类似：  */

// 用于读取网络数据
InputStream input = sock.getInputStream();

// 用于写入网络数据
OutputStream output = sock.getOutputStream();


/*
最后我们重点来看看，为什么写入网络数据时，要调用flush()方法。

如果不调用flush()，我们很可能会发现，客户端和服务器都收不到数据，这并不是Java标准库的设计问题，而是我们以流的形式写入数据的时候，并不是一写入就立刻发送到网络，而是先写入内存缓冲区，直到缓冲区满了以后，才会一次性真正发送到网络，这样设计的目的是为了提高传输效率。如果缓冲区的数据很少，而我们又想强制把这些数据发送到网络，就必须调用flush()强制把缓冲区数据发送出去。

*/



/*=========================================================


#	----- TCP编程 の 小结 ----- 

使用Java进行TCP编程时，需要使用Socket模型：

	· 服务器端Server用ServerSocket监听指定端口;

	· 客户端Client使用Socket(Network_Address, port)连接服务器;

	· 服务器端Server用accept()接收连接并返回Socket;

	· 双方通过Socket打开InputStream/OutputStream读写数据;

	· 服务器端Server通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;

	· flush()用于强制输出缓冲区到网络。



*/






